CodeBuildでRustをコンパイルしてメール経由で配布してみた
初めに
我が家ではメインの利用はWindows機となるのですが、そのマシンには開発環境のようなものはできる限り導入せず開発はMac環境で行っています。
一部Rustで開発しているものでWindows機でも利用するアプリがあるのですが、上記の関係でMac上でクロスコンパイルを行いGoogleドライブに手動でアップロードした上でダウンロードという形で行っていました。
数ヶ月に1回程のためSambaの受け口を開けたり、エージェントやスケジューラーを組み込んだりしたくはないのですが、とはいえもう少し楽したいと思い立った結果メールで配布することにしました(メーラーは常につけっぱなしの為)
構成
指定したリポジトリのmasterブランチにpushを行うとCodeBuild側でビルドを開始し、その結果をSNSを利用してメール経由で配信するようにしています。
Github Actionsでも良かったのですが、どちらにしろ一定量AWS側の設定を行う関係でAWS側にまとめてしまうのが楽そうなためCodeBuildを採用しました。
今回ビルド成果物がシングルバイナリのためLambdaの呼び出しはS3イベント通知を利用していますが(バケット名とキーがeventから取れるので楽)、複数配置が発生するようなケースではファイルごとにイベントが発生してしまうのでCodeBuildの完了をイベントにLambdaを起動する必要が出てきます。
またバイナリはサイズによっては受信側のメールサイズの上限を超えてしまい受信で気なくなるリスクがあるため、直接添付するのではなくS3のリンクを送りダウンロードする形式を採用しました。
設定
手動設定(Githubの認証)
CodeBuildとGithubとの連携のための認証は今回OAuthを利用していますが、この認証の場合は事前にマネジメントコンソール上から認証操作を行う必要があります。
プロジェクトの作成画面の以下の箇所より認証を行います。
一度認証を行ってしまえばそのプロジェクト自体の作成を完了せずとも認証状態は残るようなので、認証を実行したプロジェクトは作成を完了せずキャンセルしても問題ありません。
CloudFormation
今回実行を行う環境はWindowsのためWindows環境のビルドを利用しても良かったのですが、Windows環境の提供はリージョンやインスタンスタイプが限定的になるのでLinux環境でクロスコンパイルを実行する形を選択しました。
少し長いのでテンプレートは以下にたたみ込んでおきます。
テンプレート
AWSTemplateFormatVersion: 2010-09-09 Parameters: AppName: Type: String RepoUrl: Type: String ArtifactNotificationAddress: Type: String Resources: #---------------------- #--- CodeBuild #---------------------- BuildProject: Type: AWS::CodeBuild::Project Properties: Name: !Sub ${AppName}-builder Artifacts: Type: S3 Location: !Ref ArtifactBucket OverrideArtifactName: True Cache: Type: NO_CACHE Environment: Type: LINUX_CONTAINER ComputeType: BUILD_GENERAL1_SMALL Image: aws/codebuild/standard:7.0 LogsConfig: CloudWatchLogs: Status: ENABLED ServiceRole: !GetAtt CodeBuildRole.Arn Source: Auth: Type: OAUTH Type: GITHUB Location: !Ref RepoUrl BuildSpec: | (後述) Triggers: BuildType: BUILD Webhook: True FilterGroups: - - Type: EVENT Pattern: PUSH - Type: HEAD_REF Pattern: refs/heads/master Visibility: PRIVATE CodeBuildRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${AppName}-codebuild-role Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: codebuild.amazonaws.com Condition: StringEquals: aws:SourceAccount: !Ref AWS::AccountId Policies: - PolicyName: artifact-delivery-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 's3:PutObject' Resource: !Sub ${ArtifactBucket.Arn}/* - PolicyName: log-create-and-writer-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${AppName}-builder - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${AppName}-builder:* #---------------------- #--- S3 #---------------------- ArtifactBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${AppName}-builded-object-${AWS::AccountId}-bucket PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True BucketEncryption: ServerSideEncryptionConfiguration: - BucketKeyEnabled: True ServerSideEncryptionByDefault: SSEAlgorithm: AES256 NotificationConfiguration: LambdaConfigurations: - Event: s3:ObjectCreated:Put Function: !GetAtt NotificationFunction.Arn Filter: S3Key: Rules: - Name: prefix Value: x86_64-pc-windows-gnu/ DependsOn: - NotificationFunctionPermission #---------------------- #--- Lambda #---------------------- NotificationFunction: Type: AWS::Lambda::Function Properties: Description: "-" FunctionName: !Sub ${AppName}-artifact-notification-func Handler: "index.lambda_handler" Role: !GetAtt NotificationFunctionRole.Arn Runtime: "python3.10" Timeout: "60" Code: ZipFile: | (後述) Environment: Variables: TOPIC_ARN: !Ref ArtifactNotificationTopic NotificationFunctionRole: Type: AWS::IAM::Role Properties: Path: / RoleName: !Sub ${AppName}-artifact-notification-func-role AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Principal: Service: lambda.amazonaws.com Policies: - PolicyName: sns-publisher-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'sns:publish' Resource: !Ref ArtifactNotificationTopic ManagedPolicyArns: - !Sub "arn:aws:iam::aws:policy/AWSLambdaExecute" NotificationFunctionPermission: Type: "AWS::Lambda::Permission" Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt NotificationFunction.Arn Principal: s3.amazonaws.com SourceArn: !Sub arn:aws:s3:::${AppName}-builded-object-${AWS::AccountId}-bucket #---------------------- #--- SNS #---------------------- ArtifactNotificationTopic: Type: AWS::SNS::Topic Properties: TopicName: !Sub ${AppName}-artifact-notification DisplayName: !Sub ${AppName}-artifact-notification ArtifactNotificationEmailSubscription: Type: AWS::SNS::Subscription Properties: TopicArn: !Ref ArtifactNotificationTopic Protocol: email Endpoint: !Ref ArtifactNotificationAddress
buildspec
一部言語の環境はマネージドランタイムが存在するためその指定をすることで環境構築の手間が省けるようですが、残念ながら執筆時点ではRustのマネージドランタイムは提供されていないためビルド環境は自分で整える必要があります。
buildspecはアプリ側の管轄な気もしますが、buildspec慣れておらず調整を繰り返すことが多かったので作業上楽であったCloudFormation側に直接設置しています。
version: 0.2 env: shell: bash variables: BUILD_TARGET: x86_64-pc-windows-gnu phases: install: commands: - apt update && apt install -y mingw-w64 curl - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source $HOME/.cargo/env - rustup target add $BUILD_TARGET pre_build: comands: - cargo test --target $BUILD_TARGET build: commands: - cargo build --release --target $BUILD_TARGET artifacts: files: - target/${BUILD_TARGET}/release/*.exe name: ${BUILD_TARGET} discard-paths: yes
本来はAmazon Linux 2環境でビルドを走らせる予定だったのですが、CodeBuildで提供されているAmazon Linux 2イメージの環境ではamazon-linux-extras
がnot foundとなり解決も手間だったのでUbuntuイメージを利用しています。
Amazon Linux 2環境でRustをWindows向けにクロスコンパイルする場合はepel
をなんとかして入れてmingw64-gcc
とmingw64-winpthreads-static
をインストールする形になるはずです。
※イメージ
commands: - amazon-linux-extras install -y epel - yum install -y gcc mingw64-{gcc, winpthreads-static} - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source $HOME/.cargo/env - rustup target add $BUILD_TARGET
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-caching.html#caching-local
ローカルキャッシュは、そのビルドホストのみが利用できるキャッシュをそのビルドホストにローカルに保存します。キャッシュはビルドホストですぐに利用できるため、この方法は大規模から中間ビルドアーティファクトに適しています。ビルドの頻度が低い場合、これは最適なオプションではありません。
今回は実行頻度がごく低いのでキャッシュの利用やECRへのinstallセクションを済ませたイメージの保持は行っていません。
Lambdaコード
機密性も高くなく自宅のIPは固定なのでバケットポリシー側でaws:SourceIp
で制御をかけて通知だけでも良かったのですが、なんとなく永続的にアクセス可能なリンクを作りたくなかったので署名つきURLを発行した上でメール本文に載せることにしました。
import boto3 from botocore.client import Config import os s3 = boto3.client('s3', config=Config(signature_version='s3v4')) sns = boto3.client('sns') def lambda_handler(event, context): key = event['Records'][0]['s3']['object']['key'] bucket = event['Records'][0]['s3']['bucket']['name'] presigned_url = s3.generate_presigned_url( ClientMethod = 'get_object', Params = { 'Bucket': bucket, 'Key': key } ) sns.publish( TopicArn=os.environ['TOPIC_ARN'], Subject="[Completed] Build {}".format(key), Message=presigned_url )
なおIAMロールを利用して発行する場合署名付きURLのリンクはロールの有効期限が切れるまで(設定次第で最大12時間)となるため、関係者等に配る関係で数日間リンクを維持させたいような場合はIAMユーザを作成しその認証情報を利用して署名する必要が出てきます(最大7日間)
今回は一息入れている間にダウンロードできるようになっていれば良い程度で考えているのでLambdaに付与されているIAMロールの認証情報で署名しています(デフォルト設定のため最大1時間)。
実行
以下の実行は実際のものではなくcargo init
で作成したサンプルプロジェクトを対象に行なっています。
masterブランチにpushすることで
#無駄にcommitを増やしたくなかったのでresetとforce pushを繰り返していました % git commit Cargo.toml -m "push test" && git push -f [master 4a43a51] push test 1 file changed, 2 insertions(+), 2 deletions(-) Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 8 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 376 bytes | 376.00 KiB/s, done. Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 remote: Resolving deltas: 100% (1/1), completed with 1 local object. To github.com:xxx + 61493a8...4a43a51 master -> master (forced update) % date 2023年 6月21日 水曜日 14時49分10秒 JST
ビルドが実行され
終了のタイミングでメールで署名付きURLが飛んでくるのでリンクを踏んで保存します。
自分一人で管理して配布先も自分なのでうっかり署名付きURL有効期限らしてしまった場合、force pushをかけて無理やりビルドを走らせるか素直にS3まで取りに行きます。
終わりに
今回は設置したい側へのエージェントの導入等に制限があり頻度も低いのであまり自動化してもメリットは大きくないかな、と思っていましたが途中まで行うだけでも非常に楽になりました。
個人的には自動化するなら全部まとめてやりたいと思う気持ちもあり、なかなか趣味環境の部分自動化に手が伸びなかった部分がありましたが、実際部分的な部分でもやってみると気持ち的にものすごい楽になるのでもし似たような感じで手が伸びていない方がいたら一度試しに手をつけてみてはいかがでしょうか。
補足
記事を書き終わった後にamazonlinux2-x86_64-standard:5.0
の環境にSession Managerで接続したところ/etc/os-release
がAmazon Linux 2023相当のものととなっていました。
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-available.html
Amazon Linux 2 aws/codebuild/amazonlinux2-x86_64-standard:4.0 al2/standard/4.0
Amazon Linux 2023 aws/codebuild/amazonlinux2-x86_64-standard:5.0 al2/standard/5.0
どうやら5.0はAmazon Linux 2のカテゴリに入っているもののAmazon Linux 2023ベースのイメージになるようです。